1

文档站的特点

既是单页面, 又是服务端渲染

正好帮我刷一下访问量... http://respo.site/
注意站点的特别之处, 绝大部分的链接都是触发页面局部渲染的,
而在特殊的 js 无法加载的情况下, 还是以链接的形式刷新页面.
所以, 整个站点既是单页面, 又是服务端渲染... 感受一下.

文档站是基于 React 写的, 用的是自己的 router 和 store 方案
源码看 https://github.com/Respo/resp...
为了做页面局部渲染, 前端运行 React 是必要的,
特殊的无 js 的场景, 对应的主要是爬虫, 方便搜索引擎抓取.
而且为了搜索引擎抓取, 中间的链接需要准确无误,

比如说我浏览页面, 在前端渲染好了 tutorial.html 这个页面
http://respo.site/guide/tutor...
这个页面是从侧边栏的链接点击, 然后修改 url 并且渲染页面的,
那么, 服务器上要保证有对应的 tutorial.html 文件提供内容,
文档实际上有几十个页面, 所以几十个 HTML 都是预先渲染好的.
只不过用户访问时, 从任何一个地址进入, 后续的点击都是局部渲染的.

isomorphic rendering(同构渲染)

Gulp 渲染和前端渲染应该是一致的

这个方案有几个特征:

  • 使用默认 Nginx 即可, 不需要额外的服务器脚本, 更快

  • 访问首屏几乎就是服务端渲染, 内容展示更快

  • 访问后续页面都是客户端渲染, 更快

除了首屏加载的 js 代码没有做分块之外, 总体感觉都比较好,
没有等待服务端渲染的时间, 也没有等待客户端加载 js 再渲染的时间,
初看这是一个比较理想的站点的优化方案.

当然, 这个页面比较简单, 没有通过 Ajax 加载 JSON 再渲染的部分,
对于服务端渲染方案来说, 一个做法是由服务器抓取这部分 JSON 然后渲染,
但是这会消耗两块时间, 1) 抓取数据时间, 2) 渲染页面的时间,
略去动态内容的话, 静态的页面完全可以在 Gulp 建构时渲染完成,
单单考虑这种情况, 我觉得用 Gulp 预渲染页面是比较切实可行的方案.

至于动态内容, 我觉得在客户端通过脚本抓取是更简单的方案,
除了 SEO 之类的场景当中会要求有动态内容, 一般都能满足需要.

实现思路

Model, View 以及路由问题

大致是这样一个过程:

  • 将整个页面看做成是一个 Component, 根据路由渲染对应内容

  • 在浏览器当中, 路由通过地址栏获取, 然后渲染 Component

  • 在 Gulp 环境, 路由通过字符串模拟, 渲染渲染页面为 HTML

  • 建构过程当中, 穷举所有路由和对应的文件名, 存储 HTML 文件

首先就是服务端很容易遇到的 Component 的加载和渲染问题,
需要处理一些浏览器依赖在 Node 环境中报错的问题,
其次是组件通过 ReactDOM API 全部渲染成为 HTML.
这部分在 React 和 Webpack 社区当中已经相对成熟.

这个步骤对路由模块有做了一些要求, 需要能在服务端运行,
原理上说, 服务端解析路由历史悠久, 方案并不存在问题,
麻烦的地方是要把前端路由模块拿到后端, 一致地运行,
而且, 最好是很容易用 Node 脚本直接去操作, 以便处理多个页面,
解耦做得好的话, 都还不错. 但是也要看具体的路由支持怎样.

router-as-view

一个很小众的路由模块

https://github.com/react-chin...

router-as-view 当中提供了 parseAddress 函数,
可以根据规则直接将路由 /a/b?c=d 转换为一份数据,
随后被放进 Store 当中, 然后 Component 直接依据 Store 进行渲染.

{parseAddress} = require 'router-as-view/lib/path'

routes = Immutable.fromJS
  home: []
  'discuss.html': []
  guide: ['entry']
  docs: ['post']

router = parseAddress path, routes
initialStore = schema.store.set 'router', router

ReactDOM.renderToString(Container(store: store}))

从而很容易进行抽象, 然后再 Gulp 里调用渲染 HTML:

gulp.task 'entries', (cb) ->
  // ...
  pages.forEach (pathname) ->
    // ...
    fs.writeFileSync "build/#{filename}.html", html(env, routerPath)
  // ...

react-router

很久没用了...

具体实现不清楚, 我只能摘录文档当中的例子:

https://github.com/ReactTrain...

import { renderToString } from 'react-dom/server'
import { match, RoutingContext } from 'react-router'
import routes from './routes'

serve((req, res) => {
  // Note that req.url here should be the full URL path from
  // the original request, including the query string.
  match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message)
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search)
    } else if (renderProps) {
      res.status(200).send(renderToString(<RoutingContext {...renderProps} />))
    } else {
      res.status(404).send('Not found')
    }
  })
})

vue-router

Vue 2 文档上没解释服务端路由的问题, 从已经发布的 Demo 看是支持的,
而且像是为了服务端渲染顺带就支持了...
没有文档不好说, 只能看下代码当中的例子来推测一下:

https://github.com/vuejs/vue-...

https://github.com/vuejs/vue-...

respo-router

respo-router 基本上是 router-as-view 的 cljs 版本, 细节还有待改进. 目前已经完成了初步的建构过程渲染的 Demo:

https://github.com/Respo/shel...
http://weibo.com/1651843872/E...

有点难看到懂, 但是大体的思路还是用组件生成 HTML, 其他部分都是脚本:

(defn html-dsl [data html-content ssr-stages router]
  (make-html
    (html {}
      (head {}
        (let [store (assoc schema/store :router router)]
          (script (:attrs {:id "store" :type "text/edn" :innerHTML (pr-str store)}))))
      (body {}
        (div {:attrs {:id "app" :innerHTML html-content}})
        (script {:attrs {:src "/main.js"}})))))

(defn generate-html [router ssr-stages]
  (let [ tree (comp-container {:router router} ssr-stages)
         html-content (make-string tree)]
    (html-dsl {:build? true} html-content ssr-stages router)))

(def dict {"post" ["post"], "about.html" [], "home" []})

(defn -main []
  (spit "target/index.html" (generate-html (parse-address "/" dict) #{:shell}))
  (spit "target/about.html" (generate-html (parse-address "/about.html" dict) #{:shell}))
  (sh "mkdir" "target/post/")
  (spit "target/post/a.html" (generate-html (parse-address "/post/a.html" dict) #{:shell}))
  (spit "target/post/b.html" (generate-html (parse-address "/post/b.html" dict) #{:shell})))

结尾

其他的方案, Angular 2 未知... 按说已经支持服务端渲染, 应该能搞定,
这只是一个套路, 跟具体的框架应该是无关的, 应该都能完成.

当然, 回到文章的核心观点, 就是要提供一个 Gulp 渲染的方案,
所有的页面入口按照路由在服务端预渲染完成, 作为一个优化,
从而让页面在观感上加载得更快.


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者